package com.panopset;

import java.util.StringTokenizer;
import static com.panopset.Util.*;

/**
 * Convert String representation of a Java method and parameters to
 * a reflection execution.
 *
 * Parameters must be String objects.
 *
 * @author Karl Dinwiddie
 */
public class ReflectionInvoker {
    private MapProvider mapProvider;
    private Object      object;
    private Class<?>    clazz;
    private String      method;
    private String      parms;

    /**
     * Use reflection to execute the given method.
     * @return String, empty String if invoked method returns void, otherwise
     * results of returned object's toString() function.
     * @throws Exception
     */
    public String exec() throws Exception {
        StringTokenizer st = new StringTokenizer(parms, ",");
        Object[] args = new String[st.countTokens()];
        Class<?>[] parmTypes = new Class<?>[st.countTokens()];
        int i = 0;
        while (st.hasMoreTokens()) {
            String key = st.nextToken();
            String val = key;
            if (mapProvider != null) {
                String s = mapProvider.get(key);
                if (s != null) {
                    val = s;
                }
            }
            if (val == null) val = key;
            args[i] = val;
            parmTypes[i] = String.class;
            i++;
        }
        Object rtn = clazz.getMethod(method, parmTypes).invoke(object, args);
        return rtn == null ? EMPTY_STRING : rtn.toString();
    }

    public static class Builder {
        private Object      object;
        private String      className;
        private Class<?>    clazz;
        private String      method;
        private String      parms;
        private MapProvider mapProvider;

        public ReflectionInvoker construct() throws Exception {
            ReflectionInvoker rtn = new ReflectionInvoker();
            rtn.object = object;
            rtn.clazz = object == null ?
                    clazz ==  null ? Class.forName(className) : clazz
                    : object.getClass();
            rtn.method = method;
            rtn.parms = parms;
            rtn.mapProvider = mapProvider;
            return rtn;
        }

        /**
         * Optional, if not specified, either className or classMethodAndParms
         * must be specified.
         * @param object Object method is to be invoked on.
         * @return Builder.
         */
        public Builder object(Object object) {
            this.object = object;
            return this;
        }

        /**
         * Optional, if not specified, either object or classMethodAndParms
         * must be specified.
         * @param className className
         * @return Builder.
         */
        public Builder className(String className) {
            this.className = className;
            return this;
        }

        /**
         * If not specified, static methods only may be invoked.
         * @param clazz
         * @return Builder.
         */
        public Builder clazz(Class<?> clazz) {
            this.clazz = clazz;
            return this;
        }

        /**
         *
         * @param method methodName
         * @return Builder.
         */
        public Builder method(String method) {
            this.method = method;
            return this;
        }

        /**
         *
         * @param parms parm1,parm2
         * @return Builder
         */
        public Builder parms(String parms) {
            this.parms = parms;
            return this;
        }

        /**
         * Optional mapProvider.  If specified, the mapProvider will be
         * checked for parameter values.  If the parameter does not
         * match any of the mapProvider's keys, then the parameter is
         * simply used by itself.
         * @param mapProvider
         * @return Builder.
         */
        public Builder mapProvider(MapProvider mapProvider) {
            this.mapProvider = mapProvider;
            return this;
        }

        /**
         *
         * @param methodAndParms method(parm1,parm2)
         * @return Builder.
         */
        public Builder methodAndParms(String methodAndParms) {
            int params_start = methodAndParms.indexOf("(");
            if (params_start < 2) {
                throw new RuntimeException("Format should be function(parms), found:" + methodAndParms);
            }
            method = methodAndParms.substring(0, params_start);
            parms = methodAndParms.substring(params_start + 1,
                    methodAndParms.length() - 1);
            return this;
        }

        /**
         * Optional, if not specified either object or className must be
         * specified.
         * @param classMethodAndParms com.company.Class.method(parms)
         * @return Builder.
         */
        public Builder classMethodAndParms(String classMethodAndParms) {
            int method_start = classMethodAndParms.lastIndexOf(".");
            String className = classMethodAndParms.substring(0, method_start);
            String methodAndParms = classMethodAndParms.substring(method_start+1);
            return className(className).methodAndParms(methodAndParms);
        }
    }

    private ReflectionInvoker() {
        // prevent instantiation outside Builder.
    }
}
